<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>贪吃蛇</title>
  </head>
  <body>
    <canvas style="border: black 4px solid"></canvas>
    <button>已结束，重新开始？</button>
    <script>
class BonusList {
    constructor(width = 100, height = 100, num = 10) {
        this.width = width
        this.height = height
        // 食物数量
        this.num = num
        // 食物列表
        this.list = []
        // 生成食物
        this.getBonus()
    }
    getBonus() {
        const x = Math.floor(Math.random() * this.width)
        const y = Math.floor(Math.random() * this.height)
        this.list.push({ pos: [x, y], color: this.getRandomColor() })
        if (this.list.length < this.num) {
            this.getBonus()
        }
    }
    getRandomColor() {
        const r = Math.floor(Math.random() * 200)
        const g = Math.floor(Math.random() * 200)
        const b = Math.floor(Math.random() * 200)
        return `rgb(${r},${g},${b})`
    }
}

class Snake {
    constructor(initialPos, len = 4) {
        // 初始位置
        this.initialPos = initialPos
        // 蛇的身体
        this.body = []
        // 蛇的长度
        this.len = len
        // 蛇的方向
        this.direction = [-1, 0]
        // 初始化蛇身
        this.initialize()
    }
    initialize() {
        // 蛇头位置
        const head = this.initialPos
        this.body = [head]
        // 利用前一个段蛇身的位置，生成后一段蛇身的位置
        for (let i = 1; i < this.len; i++) {
            this.body.push([head[0] + i, head[1]])
        }
    }
    trigger(direction) {
        const [head, second] = this.body
        if (direction) {
            const copyHead = [...head]
            // 先试探移动蛇头，观察是否在边界内
            copyHead[0] += direction[0]
            copyHead[1] += direction[1]
            // 如果在第二节蛇身位置，则是无效的移动方向
            if (copyHead[0] !== second[0] || copyHead[1] !== second[1]) {
                this.direction = [...direction]
            }
        }
        // 移动整条蛇
        // 先移动头部
        const newBody = []
        newBody.push([head[0] + this.direction[0], head[1] + this.direction[1]])
        for (let i = 1; i < this.body.length; i++) {
            newBody[i] = [...this.body[i - 1]]
        }
        // 蛇身移动
        this.body = newBody
    }
}

class Game {
    constructor(canvas, control, width = 100, height = 100, ratio = 1, frameTime = 400) {
        this.width = width
        this.height = height
        // 一个格子的大小
        this.ratio = ratio
        this.canvas = canvas
        this.control = control
        // 画板大小
        this.canvas.width = width * this.ratio
        this.canvas.height = height * this.ratio
        // 每个画面的时间
        this.frameTime = frameTime
        this.ctx = this.canvas.getContext('2d')
        const initialPos = [Math.floor(width / 2), Math.floor(height / 2)]
        // 蛇
        this.snake = new Snake(initialPos)
        // 食物
        this.bonusList = new BonusList(width, height, 700)

        // 上一帧时间
        this.lastTime = 0
        // 启动动画
        requestAnimationFrame(this.frame.bind(this))

        // 监听事件
        window.onkeydown = (e) => {
            this.handleKey(e.key)
        }

        // 状态
        this.state = 1 // 0 结束  1 运行中
        this.control.disabled = true
        this.control.onclick = () => {
            this.start()
        }
        this.score = 0
    }
    handleKey(key) {
        switch (key) {
            case 'ArrowUp':
                this.trigger([0, -1])
                break
            case 'ArrowDown':
                this.trigger([0, 1])
                break
            case 'ArrowLeft':
                this.trigger([-1, 0])
                break
            case 'ArrowRight':
                this.trigger([1, 0])
                break
        }
    }
    frame(t) {
        if (this.lastTime === 0) {
            this.lastTime = t
        } else if (t - this.lastTime > this.frameTime) {
            this.trigger()
            this.lastTime = t
        }
        requestAnimationFrame(this.frame.bind(this))
    }
    trigger(pos) {
        if (!this.state) return
        this.draw()
        this.snake.trigger(pos)
        const [head, ...restBody] = this.snake.body
        // 判断是否在边界外
        if (head[0] <= 0 || head[0] >= this.width || head[1] <= 0 || head[1] >= this.height) {
            this.over()
            return
        }
        // 判断是否撞到了蛇身
        for (let i = 0; i < restBody.length; i++) {
            if (head[0] === restBody[i][0] && head[1] === restBody[i][1]) {
                this.over()
                return
            }
        }
        // 判断是否吃到了食物
        for (let i = 0; i < this.bonusList.list.length; i++) {
            const { pos } = this.bonusList.list[i]
            if (head[0] === pos[0] && head[1] === pos[1]) {
                this.snake.body.push([...pos])
                this.bonusList.list.splice(i, 1)
                this.bonusList.getBonus()
                this.score += 1
                break
            }
        }
    }
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
        this.drawSnake()
        this.drawBonus()
        this.ctx.fillStyle = 'rgba(255,0,0,.4)'
        this.ctx.font = 40 * this.ratio + "px Georgia"
        const { width, actualBoundingBoxAscent, actualBoundingBoxDescent, } = this.ctx.measureText(this.score + '')
        const height = actualBoundingBoxAscent + actualBoundingBoxDescent
        this.ctx.fillText(this.score + '', (this.width * this.ratio - width) / 2, (this.height * this.ratio - height) / 2)
    }
    drawSnake() {
        const [head, ...restBody] = this.snake.body
        this.ctx.fillStyle = 'red'
        this.ctx.fillRect(head[0] * this.ratio, head[1] * this.ratio, this.ratio, this.ratio)
        this.ctx.fillStyle = 'green'
        for (let i = 0; i < restBody.length; i++) {
            const [x, y] = restBody[i]
            this.ctx.fillRect(x * this.ratio, y * this.ratio, this.ratio, this.ratio)
        }
    }
    drawBonus() {
        for (let i = 0; i < this.bonusList.list.length; i++) {
            const { pos, color } = this.bonusList.list[i]
            this.ctx.fillStyle = color
            this.ctx.fillRect(pos[0] * this.ratio, pos[1] * this.ratio, this.ratio, this.ratio)
        }
    }
    // 游戏结束
    over() {
        this.state = 0
        this.control.disabled = false
    }
    // 游戏开始
    start() {
        this.control.disabled = true
        this.snake.initialize()
        this.state = 1
        this.score = 0
    }
}

const game = new Game(document.querySelector('canvas'), document.querySelector('button'), 100, 100, 8, 100)
      
    </script>
  </body>
</html>
